/**
 * Copyright (c) 2008, Andrew Carter
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are 
 * permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list of 
 * conditions and the following disclaimer. Redistributions in binary form must reproduce 
 * the above copyright notice, this list of conditions and the following disclaimer in 
 * the documentation and/or other materials provided with the distribution. Neither the 
 * name of Andrew Carter nor the names of contributors may be used to endorse or promote 
 * products derived from this software without specific prior written permission. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 
 * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.acarter.scenemonitor.dialog;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.ToolTipManager;
import javax.swing.border.EtchedBorder;
import javax.swing.event.TreeModelEvent;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import com.acarter.jmejtree.JMEJTree;
import com.acarter.jmejtree.JMEJTreeModel;
import com.acarter.scenemonitor.information.A_MonitorInformationPanel;
import com.acarter.scenemonitor.util.Constants;
import com.acarter.scenemonitor.util.MonitorTreeMouseHandler;
import com.acarter.scenemonitor.util.SceneMonitorProperties;
import com.acarter.scenemonitor.util.SelectPass;
import com.jme.renderer.Renderer;
import com.jme.scene.Node;
import com.jme.scene.Spatial;

/**
 * The main dialog of the tool, which contains the tree and the information.
 * 
 * @author Andrew Carter
 */
public class MonitorDialog extends A_ToolDialog {

	/** Default */
	private static final long serialVersionUID = 1L;

	/** Logger Name */
	public static final String LOGGER_NAME = "com.acarter.scenemonitor.dialog.MonitorDialog";

	/** Logger */
	private static final Logger LOGGER = Logger.getLogger(MonitorDialog.LOGGER_NAME);

	/** Tree structure representing the scene graph */
	private JMEJTree tree;

	/** List of node which may be selected for display */
	private JComboBox nodeSelection;

	/** Set of tree models for each node */
	private HashMap<Node, NodeModelWrapper> registeredNodes;

	/** Interface for the current information display type */
	private A_MonitorInformationPanel informationPanel;

	/** The refresh rate of the tree, if updated by the host application */
	private float updateInterval = 0.25f;

	/** Flag to stop/start tree updates */
	private boolean allowUpdate = false;

	/** Used to track the amount of elapsed time */
	private float timer = 0.0f;

	/** Render pass used to highlight the select object or branch */
	private SelectPass selectPass = new SelectPass();

	/** The root of the selection to highlight */
	private Spatial selectedSpatial = null;

	/** Icon for the highlight toggle button */
	ImageIcon highlightOn = null;

	/** Icon for the highlight toggle button */
	ImageIcon highlightOff = null;

	/** Flag for the highlight feature */
	private boolean highlightEnabled = false;

	/**
	 * Constructor.
	 * 
	 * @param owner
	 *            The parent of the dialog
	 */
	public MonitorDialog(Frame owner) {

		super(owner, "jME Scene Graph", false);
		
		highlightEnabled = SceneMonitorProperties.getProperties().getBoolean("Highlight");

		build();
		pack();

		// We need to wait to allow updates for the pack() to complete
		javax.swing.SwingUtilities.invokeLater(new Runnable() {

			public void run() {
				allowUpdate = true;
			}
		});

		// noel lynch 21-08-08 : changed for java 5 compatability
		setModal(false);
		//setModalityType(Dialog.ModalityType.MODELESS);
		setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
		loadLocation();

		registeredNodes = new HashMap<Node, NodeModelWrapper>();
	}

	/**
	 * 
	 */
	@SuppressWarnings("unchecked")
	private void initInformationPanel() {

		String[] knownImplementations = { "com.acarter.scenemonitor.propertydescriptor.PropertyInformation",
				"com.acarter.scenemonitor.textdescriptor.TextInformation", "com.acarter.scenemonitor.information.DefaultInformationPanel" };

		for (String classname : knownImplementations) {

			try {

				Class<?> cls = Class.forName(classname);

				if (A_MonitorInformationPanel.class.isAssignableFrom(cls)) {

					Class<? extends A_MonitorInformationPanel> factoryCls = (Class<? extends A_MonitorInformationPanel>) cls;

					Constructor<? extends A_MonitorInformationPanel> constructor = factoryCls.getConstructor();
					constructor.setAccessible(true);

					informationPanel = constructor.newInstance();

					LOGGER.info("Information panel implementation '" + classname + "' is being used.");
					break;
				}
			}
			catch (ClassNotFoundException e) {
				LOGGER.info("Information panel implementation '" + classname + "' not present.");
			}
			catch (Exception e) {
				LOGGER.log(Level.WARNING, "Failed to use information panel implementation '" + classname + "' due to Exception/Error: " + e, e);
			}
		}
	}

	/**
	 * Builds the content pane of the dialog.
	 */
	private void build() {

		final JPanel panel = new JPanel();
		panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

		panel.add(buildControlPanel());
		panel.add(buildTreePanel());

		initInformationPanel();

		JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, panel, informationPanel.buildInformationPanel());
		splitPane.setOneTouchExpandable(true);
		splitPane.setResizeWeight(0.5);
		splitPane.setPreferredSize(new Dimension(300, 600));
		splitPane.setDividerLocation(300);

		setContentPane(splitPane);
		setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
		
		loadSize();
	}

	/**
	 * Builds the upper control panel for the content pane.
	 * 
	 * @return The panel
	 */
	private JPanel buildControlPanel() {

		JPanel controlPanel = new JPanel();
		controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.X_AXIS));
		controlPanel.setPreferredSize(new Dimension(300, 24));
		controlPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 24));

		final JPanel buttonPanel = new JPanel();
		buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
		buttonPanel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createEmptyBorder(1, 1, 0, 0), BorderFactory
				.createEtchedBorder(EtchedBorder.LOWERED)));
		buttonPanel.setPreferredSize(new Dimension(17, 28));
		buttonPanel.setMaximumSize(buttonPanel.getPreferredSize());

		URL imageURL = A_ToolDialog.class.getClassLoader().getResource(Constants.DIALOGDATAPATH + "highlight_on.png");

		if (imageURL != null)
			highlightOn = new ImageIcon(imageURL);

		imageURL = A_ToolDialog.class.getClassLoader().getResource(Constants.DIALOGDATAPATH + "highlight_off.png");

		if (imageURL != null)
			highlightOff = new ImageIcon(imageURL);

		final JLabel highlightButton = new JLabel();
		highlightButton.setToolTipText("Toggles highlighting of selected node/branch.");

		if (highlightEnabled)
			highlightButton.setIcon(highlightOn);
		else
			highlightButton.setIcon(highlightOff);

		highlightButton.addMouseListener(new MouseListener() {

			public void mouseClicked(MouseEvent arg0) {

				highlightEnabled = !highlightEnabled;

				if (highlightEnabled)
					highlightButton.setIcon(highlightOn);
				else
					highlightButton.setIcon(highlightOff);
				
				SceneMonitorProperties.getProperties().put("Highlight", highlightEnabled);
			}

			public void mouseEntered(MouseEvent arg0) {
			}

			public void mouseExited(MouseEvent arg0) {
			}

			public void mousePressed(MouseEvent arg0) {

				buttonPanel.setBackground(new Color(184, 207, 229));
			}

			public void mouseReleased(MouseEvent arg0) {

				buttonPanel.setBackground(new Color(238, 238, 238));
			}
		});

		buttonPanel.add(Box.createGlue());
		buttonPanel.add(highlightButton);
		buttonPanel.add(Box.createGlue());

		nodeSelection = new JComboBox();
		nodeSelection.setEditable(false);
		nodeSelection.setToolTipText("Lists the nodes that can be monitored.");

		nodeSelection.addActionListener(new ActionListener() {

			public void actionPerformed(ActionEvent e) {

				Object object = nodeSelection.getSelectedItem();

				if ((object != null) && (object instanceof NodeModelWrapper)) {

					NodeModelWrapper w = (NodeModelWrapper) object;

					tree.setModel(w.treeModel);
				}
			}
		});

		controlPanel.add(nodeSelection);
		controlPanel.add(buttonPanel);

		return controlPanel;
	}

	/**
	 * Build the tree panel for the content pane.
	 * 
	 * @return The panel
	 */
	private JPanel buildTreePanel() {

		tree = new JMEJTree(null);
		tree.setFont(new Font("Helvetica", Font.PLAIN, 11));
		tree.setVisibleRowCount(-1);
		tree.setExpandsSelectedPaths(true);

		tree.addMouseListener(new MonitorTreeMouseHandler(this));

		ToolTipManager.sharedInstance().registerComponent(tree);

		JScrollPane treeScroller = new JScrollPane(tree);

		JPanel listPanel = new JPanel();
		listPanel.setLayout(new BoxLayout(listPanel, BoxLayout.Y_AXIS));
		listPanel.add(treeScroller);

		return listPanel;
	}

	/**
	 * Rebuilds the list of possible node selections for the user.
	 */
	private void refreshNodeSelection() {

		Object item = nodeSelection.getSelectedItem();

		nodeSelection.removeAllItems();

		for (NodeModelWrapper w : registeredNodes.values())

			nodeSelection.addItem(w);

		if (item != null)
			nodeSelection.setSelectedItem(item);
	}

	/**
	 * Refreshes the tree view.
	 */
	public void doUpdate(float tpf) {

		timer += tpf;
		selectPass.updateTime(tpf);

		if (!isVisible() || !allowUpdate)
			timer = 0.0f;
		else if (timer >= updateInterval) {

			JMEJTreeModel treeModel = (JMEJTreeModel) tree.getModel();

			if (treeModel != null) {

				TreePath selectedPath = tree.getSelectionPath();

				TreePath updatePath = new TreePath(treeModel.getRoot());
				Enumeration<TreePath> expandedPaths = tree.getExpandedDescendants(updatePath);

				if (selectedPath == null)
					selectedPath = new TreePath(treeModel.getRoot());

				TreeModelEvent evt = new TreeModelEvent(this, updatePath);
				treeModel.fireTreeStructureChanged(evt);

				tree.collapsePath(updatePath);

				while ((expandedPaths != null) && (expandedPaths.hasMoreElements())) {

					TreePath path = expandedPaths.nextElement();
					tree.expandPath(path);
				}

				tree.setSelectionPath(selectedPath);
			}

			updateInfo();

			timer = 0.0f;
		}
	}

	/**
	 * Updates the information panel for the selected tree object.
	 */
	public void updateInfo() {

		TreePath selectedPath = tree.getSelectionPath();
		Object object = null;

		if (selectedPath != null)
			object = selectedPath.getLastPathComponent();

		informationPanel.updateInformation(object);
	}

	/**
	 * 
	 * @param object
	 */
	public void updateRender() {

		TreePath selectedPath = tree.getSelectionPath();
		Object object = null;

		if (selectedPath != null)
			object = selectedPath.getLastPathComponent();

		if (object instanceof Spatial) {

			while (selectPass.size() > 0)
				selectPass.remove(selectPass.get(0));

			selectedSpatial = (Spatial) object;
			selectPass.add(selectedSpatial);
			selectPass.resetTime();
			selectPass.setEnabled(true);
		}
		else {
			selectPass.setEnabled(false);
			selectedSpatial = null;
		}
	}

	/**
	 * 
	 * @param renderer
	 */
	public void render(Renderer renderer) {

		if (highlightEnabled)
			selectPass.renderPass(renderer);
	}

	/**
	 * We do not want to update the tree while dragging.
	 */
	protected void onBeginDragWindow() {

		allowUpdate = false;
	}

	/**
	 * Allow updates again.
	 */
	protected void onEndDragWindow() {

		allowUpdate = true;
		
		saveLocation();
	}

	/**
	 * We do not want to update the tree while resizing.
	 */
	@Override
	protected void onBeginResizeWindow() {

		allowUpdate = false;
	}

	/**
	 * Allow updates again.
	 */
	@Override
	protected void onEndResizeWindow() {

		allowUpdate = true;
		
		saveSize();
	}

	/**
	 * This sets the amount of time between tree updates.
	 * 
	 * @param seconds
	 */
	public void setUpdateInterval(float seconds) {

		updateInterval = seconds;
	}

	/**
	 * Adds the node to the list of nodes the user can select.
	 * 
	 * @param node
	 *            The node to add
	 * @param displayName
	 *            The name to use in the list
	 */
	public void registerNode(Node node, String displayName) {

		NodeModelWrapper w = new NodeModelWrapper();
		w.displayName = displayName;
		w.treeModel = new JMEJTreeModel(node);

		registeredNodes.put(node, w);

		refreshNodeSelection();
	}

	/**
	 * Removes a node from the list of nodes a user can select.
	 * 
	 * @param node
	 *            The node to remove.
	 */
	public void unregisterNode(Node node) {

		if (registeredNodes.containsKey(node)) {

			NodeModelWrapper w = registeredNodes.remove(node);

			if (w.equals(nodeSelection.getSelectedItem()))
				tree.setModel(null);
		}

		refreshNodeSelection();
	}

	/**
	 * Selects the given object in the tree.
	 * 
	 * @param object The object to select.
	 */
	public void selectObject(Object object) {
		
		JMEJTreeModel treeModel = (JMEJTreeModel)tree.getModel();
		
		TreePath path = treeModel.getPathToRoot(object);
		tree.setSelectionPath(path);
		
		updateInfo();
		updateRender();
	}
	
	/**
	 * Returns the interface which handles the information panel.
	 * 
	 * @return The descriptor handler
	 */
	public A_MonitorInformationPanel getInformationPanel() {

		return informationPanel;
	}

	/**
	 * Utility class to bundle the display name of the node with its tree model.
	 * 
	 * @author Carter
	 */
	public class NodeModelWrapper {

		public String displayName;

		public TreeModel treeModel;

		@Override
		public String toString() {

			return displayName;
		}
	}

	/**
	 * Shows the help/about dialog to the user.
	 */
	@Override
	public void onHelp() {

		AboutDlg about = new AboutDlg(null);

		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
		Dimension frameSize = about.getSize();

		if (frameSize.height > screenSize.height) {
			frameSize.height = screenSize.height;
		}
		if (frameSize.width > screenSize.width) {
			frameSize.width = screenSize.width;
		}

		about.setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);

		about.setVisible(true);
	}
	
	/**
	 * 
	 *
	 */
	public void loadLocation() {
		
		int x = SceneMonitorProperties.getProperties().getInt("X");
		int y = SceneMonitorProperties.getProperties().getInt("Y");
		setLocation(x, y);
	}
	
	/**
	 * 
	 *
	 */
	public void saveLocation() {
		
		Point loc = getLocation();
		SceneMonitorProperties.getProperties().put("X", loc.x);
		SceneMonitorProperties.getProperties().put("Y", loc.y);
	}
	
	/**
	 * 
	 *
	 */
	public void loadSize() {
		
		int height = SceneMonitorProperties.getProperties().getInt("Height");
		int width = SceneMonitorProperties.getProperties().getInt("Width");
		
		if(height != 0 && width != 0)
			getContentPane().setPreferredSize(new Dimension(width, height));
	}
	
	/**
	 * 
	 *
	 */
	public void saveSize() {
		
		Dimension dim = getContentPane().getPreferredSize();
		SceneMonitorProperties.getProperties().put("Height", dim.height);
		SceneMonitorProperties.getProperties().put("Width", dim.width);
	}
}
